《C++面向对象软件设计及构建》文章翻译:2.3创建及操作一个对象,2.3 Creating and Operating on an Object
执行操作
在某个类的公有接口中定义的那些操作,可以在该类的任意对象上执行。继续观摩Frame 类的示例,可以通过以下代码来创建及操作一个Frame 对象:
Frame display("Test Window", 10,20, 100, 200);
display.MoveTo(50, 50);
display.Resize(200,200);
display.DrawText("Really Neat!", 50,50);
此处 ,声明了一个名为 display 的Frame 对象。 Frame 的构造函数会被隐式地调用,并且会传入声明语句中提供的参数。 在这些构造函数参数的作用下,会创建一个Frame,它的左上角位于(10,20),它的高度和宽度分别是100 和200。
当display 对象创建完毕之后,就可以对它做操作了。在这个示例中,首先使用 MoveTo 方法来display 将对象移动到(50,50)位置,再使用 Resize 方法将这个Frame 的形状变成200 x 200 的正方形。最后,在Frame 中画出文字"Really Neat!",它的位置是,相对于Frame 的左上角的(50,50)位置。
注意 , "."(小数点)操作符 ,用于调用某个对象上的某个方法。因此 , display.MoveTo(...) ,表示的是,调用 display 对象上的 MoveTo 方法。使用面向对象编程语言 的人士, 经常会这样说,“ 让display 对象移动自己 的 位置 ”, 以表 示 出 在对象上进行的操作 。 这个,反映了这样一种观点,那就是,对象 ,就是某个能够做出特定动作的实体 (例如,Frame对象知道如何 将自己移动到一个新的位置 ) 。
简单编程环境
对图形用户界面对象进行编程(例如此处的Frame 类,以及日后要说的其它类),需要用到一个编程环境,这个编程环境,与那些入门级编程课程中的编程环境有显著的不同。编程环境的不同之处,在于这样一个事实,就是,图形用户界面系统,是 事件驱动 或者说 响应 式 的系统,这就意味着,系统是由外部事件(鼠标点击、时钟变动)驱动的,它必须对这些事件做出响应。一个事件驱动或响应式的系统,其典型的生命周期如下:
1.接收到通知,告知某个事件已发生。
2.利用与当前系统状态相关的信息来决定,该如何对该事件做出响应。
3.对该事件做出响应,具体就是,更新界面上的显示,并且改变系统的状态信息。
4.返回并等待下一个通知。
当有输入信息产生,或者控制动作发生时,程序并不会读入这些输入信息或控制信息,它只是对它们的出现做出响应。下面给出一些简单示例,示例中会使用Frame 类的对象。
图形用户界面中的事件,来自两个不同的来源:一个来源是用户;另一个来源是硬件时钟。在一个典型的工作站环境中,用户通过以下方式来产生事件:移动鼠标;按下及松开鼠标按钮;或者,按下键盘上的按键。如果装有其它外设的话,用户还能够通过以下方式来产生事件:移动游戏控制杆;移动轨迹球;或者,移动虚拟现实设备(手套、头盔显示器,等等)。在我们这第一个简单的编程环境中,只会对鼠标事件做出响应。硬件时钟,能够产生时序事件,这种事件,会用于在动画式的用户界面中创建动画式组件。对于这种动画式组件,有一个简单的例子,就是,闪烁的文字,它会将用户的注意力吸引到界面上某个重要部位。在程序中,要想让文字能够闪烁的话,程序本身就必须能够测量时间的流逝。一个流式或序列式的定时器事件组,就能够提供这种能力。
此处,我们会使用一个简单的编程环境,它与那些在常见的面向对象窗口系统(它与这个东西非常近似:Java抽象窗口工具包(Java Abstract Windowing Toolkit)(事实上,它与那个东西的基本模式是等同的)。)中出现的编程环境类似。然而,最初版本的编程环境,却不是面向对象的。随着妳学习更多关于面向对象编程的知识,我们会将这个环境中的各个部分逐渐改写成更好的面向对象形式。最终,整个图形用户界面程序,都会变成由一个应用程序对象来表示。
需要注意,这个简单的环境中,并没有主程序。面向对象的编程语言,包括C++在内,都拥有一个主程序,其中定义了执行代码的起点,并且,在狠多程序中,主程序都是由应用程序开发者编写的。然而,在图形用户界面程序中(以及其它的,使用了较复杂的运行时库的程序中),主程序通常都是由运行时库的实现者来编写的,该运行时库提供了狠多的底层支持函数。
这个简单编程环境中,有4个过程函数,位于同一个文件中,文件名为Program.cc,如下所示。在那些过程函数之外,即是一个全局的作用域,在其中,可声明整个程序级别的对象(例如长期存在的Frame对象)。在这个全局作用域中,还可以定义一些用于标示应用程序当前状态的变量。在那4个过程函数中,都可对这些全局对象和全局变量做出操作。注意,这里的文件名,Program.cc,是随意选取的,它在C++或图形用户界面编程中并没有什么一般意义。
简单编程环境 |
// 文件Program.cc #include "Program.h" // 在此处包含任何必要的头文件(例如"Frame.h") // 在此处定义任何全局对象或全局变量 void OnStart(void) {} void OnMouseEvent(char *frameName, int x, int y, int buttonState){} void OnTimerEvent(void){} void OnPaint( void ){} |
OnStart函数,仅会被调用一次。它可用于对任何的全局对象和数据进行初始化,以及,创建即将被用户看到的初始显示内容。每当系统认为,用户界面可能已经损坏、应当重绘的时候,就会调用OnPaint 方法。常见的能够触发这个函数的动作包括:窗口被移动;窗口改变大小;之前被某个窗口部分地或全部地遮挡,如今变得可见了。每当用户在Frame 对象的显示区域中点击鼠标按钮或者移动鼠标时,就会调用OnMouseEvent 函数。这个函数,有若干个输入参数:事件在其中发生的那个Frame 对象的名字;鼠标当前位置的 x 和 y 坐标;鼠标按钮的当前状态。每当时钟事件发生时,就会调用OnTimerEvent 函数。下表中,简短地说明了,在简单编程环境中,这4个函数的作用。
简单编程环境中的函数概要 |
|
OnStart |
初始化全局对象、全局变量和用户界面显示内容 |
OnPaint |
在必要的时候,重绘用户界面 |
OnTimerEvent |
对时钟事件做出响应 |
OnMouseEvent |
对鼠标点击和/或鼠标移动事件做出响应 |
尽管 ,常规的(通常也是强烈的且正确的)建议中,要求避免使用全局数据,但是,存在 着两个原因,使得,在简单编程环境中需要使用全局变量。第一, 这些全局变量,只是一个临时的权宜之计 - 随着 妳学习到更多关于面向对象编程的知识,狠多(即使不是所有的) 的全局数据都会消失。此时此刻 ,我们暂时忍受这些全局数据, 以便能够开始进行基本的面向对象编程实验。第二,全局 对象 ,相比于全局 变量 来说,并不是那么讨厌。最少 ,在对象中定义了一个接口,它能保护其中 封装 的数据,不被 误 用。全局数据 就没有这种保护能力了。 进一步来说, 在构建对象之间关联关系的过程中,并不总是能够完全排 除掉全局数据的。 有些时候,那些组成关联关系的对象,必须被定义为全局数据。 打个比方,将汽车看作一个关联关系,那么,其中 的某些组件就无法隐藏起来,例如方向盘、刹车 和转向灯。
在这个简单编程环境中编写程序,就需要,提供代码 ,用于实现简单编程环境中定义的4个函数中的部分或全部。 这会产生 一个可执行程序,它会使用妳所定义的这些函数。通过编辑Program.cc文件, 即可为这4个函数提供代码。然后 ,就必须编译这个文件,并且将它链接到适当的运行时库。 这个辅助步骤, 由一个现成的“制造”(" make ")文件来完成。编辑 完Program.cc 之后,直接运行"make"命令即可 ,不带任何参数。 make程序 会创建出一个可执行文件,名为" Program " 。 为了简单起见,这里的两个名字, Program.cc 和 Program ,不可改变。
在运行的时候,使用简单编程环境开发的程序,首先会显示一个开始(Start)窗口,其中提供了一些控件,可用于:初始化程序;终止程序;以及控制某个简单定时器。下图中展示了开始窗口。其中的开始(Start)按钮,在按下之后,会调用OnStart 函数,然后,开始按钮会消失。在任何时候,可通过点击文件(File)菜单中的退出(Quit)选项来终止整个程序。定时器(Timer)菜单中,包含两个条目,分别用于开启及关闭某个定时器(即为时钟事件来源)。时钟事件的产生,会导致OnTimerEvent 函数被调用。这个定时器的时间间隔(两次定时器事件之间的间隔),由带有"Timer Control"字样的滑动条控制。滑动条和定时器的时间单位是毫秒。滑动条的最初数值是500毫秒(半秒钟)。定时器时间间隔的取值范围是50毫秒(20分之1秒)到1000毫秒(1秒钟)。当定时器处于运行状态时,要想改变时间间隔的话,就必须,先停止定时器,再改变滑动条位置,再重新启动定时器。当定时器正在运行时,直接改变滑动条的值,并不会实际地改变定时器时间间隔。
开始窗口 |
|
我们提供了3个简单程序,以展示这个简单编程环境。第一个程序,是一个简单的“世界妳好”("Hello World")程序。第二个程序,在第一个“世界妳好”程序的基础上做修改,加入对鼠标按钮点击事件的处理。第三个程序,在第二个程序的基础上做修改,加入对定时器事件的使用。
下表中展示了世界妳好程序。这个程序,包含了"Frame.h"头文件,因为它需要使用Frame 类的定义来声明一个Frame 对象,其名字为 window ,位于屏幕的(200,200)处,宽度为400 像素,高度为400 像素。当这个窗口显示出来的时候,它的标题会是"Hello World Program"。在OnStart 和OnPaint 函数中,会清空窗口(window)对象的内容,并在窗口左上角附近显示字符串"Hello World"。
世界妳好程序 |
#include "Frame.h" Frame window("Hello World Program", 200, 200, 400, 400); void OnStart(void) { window.Clear(); window.DrawText("Hello World!", 20, 20); } void OnMouseEvent(char *frameName, int x, int y, int buttonState) {} void OnTimerEvent(void) {} void OnPaint(void) { window.Clear(); window.DrawText("Hello World!", 20, 20); } |
第二个程序展示了如何处理鼠标事件。在这个版本的世界妳好程序中,会在用户点击鼠标左键的地方显示出字符串"Hello World!"。每当鼠标被移动或者某个鼠标按钮被点击时,就会调用OnMouseEvent 函数。在这个程序中,OnMouseEvent 函数会检查鼠标的状态,以判断,鼠标左键是否处于“按下”状态(也就是说,被按下了)。如果那个按钮是处于按下状态(表明用户点击了鼠标按钮),那么,窗口会被清空,并将字符串"Hello World!"显示到鼠标事件发生的坐标处。OnMouseEvent 函数中的"buttonState"参数,是一个位映射。这句代码:
if( buttonState & leftButtonDown)...
即可简单地检测,表明鼠标左键被按下的那个位是否已经被设置了。如果该个位已经被设置了,则表明对应的鼠标按钮处于按下状态。此处的"leftButtonDown",是某个枚举中的一个值,它是在文件Program.h 中定义的。其它的值包括:rightButtonDown;middleButtonDown;和isDragging。在这个程序中的另一个重要的注意事项就是,使用了由变量 lastx 和 lasty 表示的状态信息,它们会记录上次显示"Hello World!"字符串的位置。对这个状态信息进行记录是有必要的,因为,当窗口需要重绘时,需要在OnPaint 函数中知道该在窗口中的哪里放置这个字符串。注意, lastx 和 lasty 是在OnStart 函数中初始化的,并在OnMouseEvent 函数中发生鼠标点击事件时更新。
带有鼠标事件处理能力的世界妳好程序 |
#include "Program.h" #include "Frame.h" Frame window("Hello World Program", 200, 200, 400, 400); int lastx; int lasty; void OnStart(void) { window.Clear(); window.DrawText("Hello World!", 20,20); lastx = 20; lasty = 20; } void OnMouseEvent(char *frameName, int x, int y, int buttonState) { if (buttonState & leftButtonDown) { window.Clear(); window.DrawText("Hello World!",x,y); lastx = x; lasty = y; } } void OnTimerEvent(void) {} void OnPaint(void) { window.Clear(); window.DrawText("Hello World!", lastx, lasty); } |
第三个程序,展示的是,如何处理定时器事件。在这个程序中,"Hello World!"字符串会变成闪烁文字,具体做法就是,在相邻的定时器事件中,交替地清除和显示出字符串。闪烁频率是由定时器的时间间隔来控制的。要注意,必须使用开始(Start)窗口中的定时器(Timer)菜单来开启定时器,才会有定时器事件产生。另外,开始窗口中的滑动条用于控制定时器事件之间的间隔。
这个程序中,还使用了另一个状态信息,它由变量 visible 来记录,记录的是,字符串当前是处于可见状态还是不可见状态。除了OnTimerEvent 之外,OnMouseEvent 和OnPaint 函数也会使用这个状态信息,以决定,是否要在窗口中显示这个字符串。
带有鼠标事件处理和定时器事件处理能力的世界妳好程序 |
#include "Program.h" #include "Frame.h" Frame window("Hello World Program", 200, 200, 400, 400); int lastx; int lasty; int visible; void OnStart(void) { window.Clear(); window.DrawText("Hello World!", 20, 20); lastx = 20; lasty = 20; visible = 1; } void OnMouseEvent(char *frameName, int x, int y, int buttonState) { if (buttonState & leftButtonDown) { window.Clear(); if (visible) window.DrawText("Hello World!",x,y); lastx = x; lasty = y; } } void OnTimerEvent(void) { window.Clear(); if (visible) visible = 0; else { visible = 1; window.DrawText("Hello World!", lastx, lasty); } } void OnPaint(void) { window.Clear(); if (visible) window.DrawText("Hello World!", lastx, lasty); } |
以下练习中,会实现一系列有意思的小程序,妳可以使用Frame 类和简单编程环境来编写它们。
任务
1.编写一个声明语句,它会创建一个Frame 对象,位置在(20,30),宽度为150,高度为175。
2.编写一砣代码,将妳在上一个步骤中创建的Frame 移动到(50,50)位置,改变它的宽度为100,高度为200。
3.编写一个程序,它会将妳的全名显示在某个Frame的正中心位置,而那个Frame 本身也会显示在屏幕的正中心位置。注意,妳可能需要多次试验,以确定适当的尺寸和位置。
4.编写一个程序,在一个尺寸为 400 x 400 的Frame 中,每个边角处,绘制一个半径为 20 的圆。
5.模拟时钟:编写一个程序,它会在某个Frame 中显示一个模拟时钟的图片。可使用一个圆来表示时钟的表盘,使用一条线段来表示指针。Place the hand pointing straight up.
6.带有两个指针的模拟时钟:修改前面的模拟时钟程序,使得它拥有一个较短的(小时数)指针和一个较长的(分钟数)指针。调整两个指针的角度和位置,使得时钟上显示的是3点。
7. 带数字的模拟时钟:修改之前的模拟时钟程序,使得,在表盘外侧,显示出1 到12 的数字。
8.动画式模拟时钟:修改之前的任何一个模拟时钟程序,使得指针会移动。在每个定时器事件发生时,将指针移动到下一个位置。
9.角落漫游:编写一个程序,它将一个圆放置在某个Frame的左上角。然后,在程序中,应当将这个圆移动到右上角、右下角和左下角,最后再将它移动到最初位置。将这个循环重复10次。在位于每个位置时,在窗口中适当位置显示一行文字,表明圆的位置,例如"Upper Left"" Upper Right""Lower Right""Lower Left"。
10. 边境游走:编写一个程序,它让一个圆以较小的步幅沿着屏幕边缘移动,起点是屏幕的左上角。妳能否做到,让移动过程显得流畅?
猴骑猪
Melissa Satta
Your opinionsHxLauncher: Launch Android applications by voice commands